package uk.ac.cam.echo.social;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.DialogError;
import com.facebook.android.Facebook;
import com.facebook.android.Facebook.DialogListener;
import com.facebook.android.FacebookError;

/**
 * This class represents a limited interface to facebook sufficient for the
 * needs of the project. WARNING: Do NOT use instances of this class until
 * authorised() has been called on it!
 * 
 * @author Vladimir Kovalev
 */
public class FacebookClient
{
	private static FacebookClient instance;
	private final String progId;
	private final String cacheFilename = "facebook_SSO_cache";
	private final String groupId;
	private final Facebook fb;
	private final AsyncFacebookRunner asyncFB;
	private SharedPreferences mPrefs;

	/**
	 * @param _groupId
	 *            a String associated with a particular group on FB
	 * @param _progId
	 *            a String associated with this particular FB application,
	 *            provided by FB upon application's registration
	 * @param _errorListener
	 *            a StringEventListener which will be called when one of the
	 *            async methods, while not ideal, is fairly easy to use. Later
	 *            could be changed to an ExceptionListener, if this is desired
	 *            please let Vlad know.
	 */
	private FacebookClient(String _groupId, String _progId)
	{
		groupId = _groupId;
		progId = _progId;
		fb = new Facebook(progId);
		asyncFB = new AsyncFacebookRunner(fb);
	}

	/**
	 * This method is required for the object initialisation
	 * 
	 * @param _groupId
	 *            a String associated with a particular group on FB
	 * @param _progId
	 *            a String associated with this particular FB application,
	 *            provided by FB upon application's registration
	 * @param _errorListener
	 *            a StringEventListener which will be called when one of the
	 *            async methods, while not ideal, is fairly easy to use. Later
	 *            could be changed to an ExceptionListener, if this is desired
	 *            please let Vlad know.
	 */
	public static void init(String _groupId, String _progId)
	{
		if (instance == null)
			instance = new FacebookClient(_groupId, _progId);
	}

	/**
	 * This method is used to access a singleton copy of FacebookClient
	 * 
	 * @return Returns the only copy of FacebookClient if .init() method has
	 *         been called if not throws
	 */
	public static FacebookClient getInstance()
	{
		return instance;
	}

	/**
	 * This method provides the caller with the ID of the facebook user who is
	 * currently logged in. This method is synchronous.
	 * 
	 * @return Facebook ID of the logged in user
	 * @throws NotAuthorisedException
	 *             is thrown if the nobody is logged in, in other words the
	 *             authorise() method has not been called or is currently being
	 *             executed
	 * @throws IOException
	 */
	public String getUsersFacebookIDSync() throws NotAuthorisedException, IOException
	{
		String result = null;

		try
		{
			result = fb.request("me");
			JSONObject user = new JSONObject(result);
			return user.getString("id");
		}
		catch (MalformedURLException e)
		{
			// should never happen since the URL is hardcoded
			throw new IOException(e.getMessage());
		}
		catch (JSONException e)
		{
			throw new NotAuthorisedException(e.getMessage());
		}
	}

	/**
	 * This method provides the caller with the ID of the facebook user who is
	 * currently logged in via a StringEventListener} . This method is
	 * asynchronous.
	 * 
	 * @param listener
	 *            the listener which is called upon facebook ID retrieval.
	 */
	public void getUsersFacebookIDAsync(final StringEventListener listener, final StringEventListener errListener)
	{
		asyncFB.request("me", new SimplifiedRequestListener()
		{

			@Override
			public void onComplete(String response, Object state)
			{
				try
				{
					JSONObject user = new JSONObject(response);
					listener.respondToEvent(user.getString("id"));
				}
				catch (JSONException e)
				{
					errListener.respondToEvent("The user has not logged in yet, ID unavailable");
				}
			}

			@Override
			protected void runOnError(String errorText)
			{
				errListener.respondToEvent(errorText);
			}
		});
	}

	/**
	 * This method should be used to allow a user to log in. It will bring up a
	 * window where a user will be able to enter their FB details, if the user
	 * is already logged in (in their default FB app) the process will not
	 * require any actions from the user. NOTE: The method is asynchronous. None
	 * of the methods in this class can be used before this method is called.
	 * This method only needs to be called once.
	 * 
	 * @param resultListener
	 *            this listener is called when the logging in process has
	 *            finished execution.
	 */
	public void authorize(final SimpleEventListener resultListener, final StringEventListener errListener, final Activity caller)
	{
		mPrefs = caller.getSharedPreferences(cacheFilename, Activity.MODE_PRIVATE);
		String access_token = mPrefs.getString("access_token", null);
		long expires = mPrefs.getLong("access_expires", 0);
		if (access_token != null)
		{
			fb.setAccessToken(access_token);
		}
		if (expires != 0)
		{
			fb.setAccessExpires(expires);
		}

		if (!fb.isSessionValid())
		{
			fb.authorize(caller, new String[] { "publish_stream", "user_photos", "user_groups" }, new DialogListener()
			{
				@Override
				public void onComplete(Bundle values)
				{
					if (values.containsKey("error"))
					{
						errListener.respondToEvent("Problem logging into FB\nAdditional info:\n" + values);
					}
					else
					{
						SharedPreferences.Editor editor = mPrefs.edit();
						editor.putString("access_token", fb.getAccessToken());
						editor.putLong("access_expires", fb.getAccessExpires());
						editor.commit();
						resultListener.respondToEvent();
					}
				}

				@Override
				public void onFacebookError(FacebookError error)
				{
					errListener.respondToEvent(error.getMessage());
				}

				@Override
				public void onError(DialogError e)
				{
					errListener.respondToEvent(e.getMessage());
				}

				@Override
				public void onCancel()
				{
					errListener.respondToEvent("The user has canceled authentication.");
				}
			});
		}
		else
		{
			resultListener.respondToEvent();
		}
	}

	/**
	 * This method needs to be called in the onActivityResult method of the
	 * Activity containing an instance of this class for facebook authorisation
	 * process to function correctly. All of the parameters are provided by the
	 * formal parameters of onActivityResult
	 * 
	 * @param requestCode
	 * @param resultCode
	 * @param data
	 */
	public void authorizeCallback(int requestCode, int resultCode, Intent data)
	{
		fb.authorizeCallback(requestCode, resultCode, data);
	}

	/**
	 * This method allows one to post a message on the wall of the user
	 * currently logged in. This method is asynchronous.
	 * 
	 * @param msg
	 *            Message content
	 * @param resultListener
	 *            This listener gets called upon message submission.
	 * @throws IOException
	 *             This exception is thrown when the method cannot reach FB
	 */
	public void postWallMessage(final String msg, final StringEventListener resultListener, final StringEventListener errListener) throws IOException
	{
		Bundle messagePostRequest = new Bundle();
		messagePostRequest.putString(Facebook.TOKEN, fb.getAccessToken());
		messagePostRequest.putString("message", msg);

		postMessage(messagePostRequest, "me", resultListener, errListener);
	}

	/**
	 * This method allows one to post a message on the event's group wall by the
	 * currently logged in user. This method is asynchronous.
	 * 
	 * @param msg
	 *            Message content
	 * @param resultListener
	 *            This listener is called upom message submission
	 * @throws IOException
	 *             This exception is thrown whenever FB can not be reached
	 */
	public void postGroupMessage(final String msg, final StringEventListener resultListener, final StringEventListener errListener) throws IOException
	{
		Bundle messagePostRequest = new Bundle();
		messagePostRequest.putString(Facebook.TOKEN, fb.getAccessToken());
		messagePostRequest.putString("message", msg);

		postMessage(messagePostRequest, groupId, resultListener, errListener);
	}

	private void postMessage(final Bundle msg, final String targetId, final StringEventListener resultListener, final StringEventListener errListener) throws FileNotFoundException, MalformedURLException, IOException
	{
		if (msg.getString(Facebook.TOKEN) == null)
		{
			errListener.respondToEvent("Not logged into Facebook, message has NOT been sent");
			return;
		}

		asyncFB.request(targetId + "/feed", msg, "POST", new SimplifiedRequestListener()
		{

			@Override
			public void onComplete(String response, Object state)
			{
				resultListener.respondToEvent(response);
			}

			@Override
			protected void runOnError(String errorText)
			{
				resultListener.respondToEvent(errorText);
			}
		}, null);

	}

	/**
	 * This method posts a picture on the event's group wall. This method is
	 * asynchronous.
	 * 
	 * @param data
	 *            Byte array representing the picture
	 * @param description
	 *            Textual description of the picture
	 * @param friendsToTag
	 *            A list of facebook user IDs who will be tagged in the picture.
	 *            By default NOBODY is tagged.
	 * @param resultListener
	 *            This listener is called upon picture's submission.
	 * @throws FacebookError
	 * @throws IOException
	 *             is thrown when internet connecion problems occur
	 * @throws JSONException
	 * @throws MalformedURLException
	 * @throws FileNotFoundException
	 */
	public void uploadPictureToGroup(final byte[] data, final String description, final List<String> friendsToTag, final StringEventListener resultListener, final StringEventListener errListener) throws FacebookError, FileNotFoundException, MalformedURLException, JSONException, IOException
	{
		final String successMessage = "The picture has been succesfully uploaded.";

		Bundle uploadPictureRequest = new Bundle();
		String fbToken = fb.getAccessToken();
		if (fbToken == null)
		{
			errListener.respondToEvent("Not logged into Facebook, picture has NOT been uploaded.");
			return;
		}
		uploadPictureRequest.putString(Facebook.TOKEN, fbToken);
		uploadPictureRequest.putString("message", description);
		uploadPictureRequest.putByteArray("source", data);

		asyncFB.request(groupId + "/photos", uploadPictureRequest, "POST", new SimplifiedRequestListener()
		{
			@Override
			public void onComplete(String response, Object state)
			{
				FacebookResponse pictureUploadResponse;
				try
				{
					pictureUploadResponse = new FacebookResponse(response);
				}
				catch (JSONException e)
				{
					errListener.respondToEvent("Invalid response from Facebook, upload failed.");
					return;
				}

				if (pictureUploadResponse == null || pictureUploadResponse.errorOccured)
				{
					errListener.respondToEvent("No response from Facebook received, picture uploading failed.");
					return;
				}

				if (friendsToTag != null && friendsToTag.size() > 0)
				{
					final IntegerWrapper peopleToTag = new IntegerWrapper(friendsToTag.size());
					final Object intLock = new Object();

					for (String friendId : friendsToTag)
					{
						Bundle tagRequest = new Bundle();
						tagRequest.putString(Facebook.TOKEN, fb.getAccessToken());
						tagRequest.putString("to", friendId);
						asyncFB.request(pictureUploadResponse.newObjectId + "/tags", tagRequest, new SimplifiedRequestListener()
						{
							@Override
							protected void runOnError(String errorMessage)
							{
								errListener.respondToEvent("An error occured while uploading the picture:\n" + errorMessage);
								return;
							}

							@Override
							public void onComplete(String response, Object state)
							{
								synchronized (intLock)
								{
									peopleToTag.set(peopleToTag.get() - 1);
									if (peopleToTag.get() == 0)
									{
										resultListener.respondToEvent(successMessage);
									}
								}
							}
						});
					}
				}
				else
				{
					resultListener.respondToEvent(successMessage);
				}
			}

			@Override
			protected void runOnError(String errorText)
			{
				errListener.respondToEvent(errorText);
			}
		}, null);
	}

	public static class NotAuthorisedException extends Exception
	{
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		public NotAuthorisedException(String s)
		{
			super(s);
		}
	}

	public boolean sessionIsValid()
	{
		return fb.isSessionValid();
	}
}
